iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
AI Engineering

從行程到應援:30 天做出追星實用工具箱系列 第 11 篇

Day 11:如何將切片音訊流暢送出並取得逐字稿

  • 分享至 

  • xImage
  •  

一、今日目標

  1. 建立 WebSocket 雙向通道:連線管理,確保連線穩定。
  2. 即時推送 Audio Chunk:將 Day 9 擷取到的音訊切片即時透過WebSocket 發送。
  3. 即時監聽與渲染逐字稿:接收伺服器邊辨識邊吐出的文字,實現邊講話畫面邊出字的效果。

二、今日任務

Step 1: 安裝 WebSocket 套件

在 pubspec.yaml 中加入 Flutter 官方維護的 WebSocket 處理套件:

dependencies:
  web_socket_channel: ^3.0.1

執行 flutter pub get

Step 2: 實作 WebSocket 串流服務 (websocket_service.dart)

建立 lib/services/websocket_service.dart,負責處理 WebSocket 的連線、傳送音訊與監聽文字:

import 'dart:async';
import 'dart:typed_data';
import 'package:web_socket_channel/web_socket_channel.dart';

class WebSocketService {
  WebSocketChannel? _channel;
  StreamSubscription? _subscription;

  /// 1. 連線至 STT WebSocket 伺服器
  Future<void> connect(String url, Function(String textChunk) onTextReceived) async {
    try {
      _channel = WebSocketChannel.connect(Uri.parse(url));
      print('WebSocket 連線成功:$url');

      // 監聽伺服器即時傳回來的逐字稿(文字流)
      _subscription = _channel!.stream.listen(
        (message) {
          print('收到即時逐字稿: $message');
          onTextReceived(message.toString());
        },
        onError: (error) => print('WebSocket 錯誤: $error'),
        onDone: () => print('WebSocket 連線已關閉'),
      );
    } catch (e) {
      print('連線失敗: $error');
    }
  }

  /// 2. 將 Day 9 的 Audio Chunk (位元組) 即時送出
  void sendAudioChunk(Uint8List chunk) {
    if (_channel != null) {
      _channel!.sink.add(chunk); // 直接把 Byte 串流塞進管道
    }
  }

  /// 3. 斷開連線與釋放資源
  Future<void> disconnect() async {
    await _subscription?.cancel();
    await _channel?.sink.close();
    _channel = null;
    print('已中斷 WebSocket 連線');
  }
}

Step 3: 整合錄音與 WebSocket 串流 (recorder_page.dart)

將 Day 9 的 AudioRecorderService 與今天的 WebSocketService 結合,實現邊錄邊傳:

import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'services/audio_recorder_service.dart';
import 'services/websocket_service.dart';

class AudioStreamPage extends StatefulWidget {
  const AudioStreamPage({super.key});

  @override
  State<AudioStreamPage> createState() => _AudioStreamPageState();
}

class _AudioStreamPageState extends State<AudioStreamPage> {
  final AudioRecorderService _recorderService = AudioRecorderService();
  final WebSocketService _webSocketService = WebSocketService();

  bool _isStreaming = false;
  String _liveTranscript = ''; // 即時顯示的逐字稿內容

  // 測試用的免費 WebSocket 模擬伺服器(或換成你自己的後端)
  final String _wsUrl = 'wss://echo.websocket.org'; 

  void _toggleStreaming() async {
    if (_isStreaming) {
      // 停止錄音與傳輸
      await _recorderService.stopRecording();
      await _webSocketService.disconnect();
      setState(() => _isStreaming = false);
    } else {
      setState(() {
        _isStreaming = true;
        _liveTranscript = ''; // 清空上次結果
      });

      // 1. 建立 WebSocket連線
      await _webSocketService.connect(_wsUrl, (String textChunk) {
        setState(() {
          // 收到邊辨識邊吐出的字,持續累加到畫面上
          _liveTranscript += textChunk; 
        });
      });

      // 2. 開始錄音,並把每個 Audio Chunk 即時丟給 WebSocket
      await _recorderService.startChunkedRecording((Uint8List chunk) {
        _webSocketService.sendAudioChunk(chunk);
      });
    }
  }

  @override
  void dispose() {
    _recorderService.dispose();
    _webSocketService.disconnect();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Day 11 - 即時語音串流逐字稿')),
      body: Padding(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          children: [
            const SizedBox(height: 30),
            Icon(
              _isStreaming ? Icons.record_voice_over : Icons.mic_none,
              size: 80,
              color: _isStreaming ? Colors.green : Colors.grey,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: _toggleStreaming,
              style: ElevatedButton.styleFrom(
                backgroundColor: _isStreaming ? Colors.red : Colors.green,
                padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
              ),
              child: Text(
                _isStreaming ? '停止串流' : '開始即時串流對話',
                style: const TextStyle(color: Colors.white, fontSize: 16),
              ),
            ),
            const SizedBox(height: 40),
            const Align(
              alignment: Alignment.centerLeft,
              child: Text('即時逐字稿顯示:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
            ),
            const SizedBox(height: 10),
            Expanded(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: Colors.black12,
                  borderRadius: BorderRadius.circular(12),
                ),
                child: SingleChildScrollView(
                  child: Text(
                    _liveTranscript.isEmpty ? '按下按鈕並開始說話...' : _liveTranscript,
                    style: const TextStyle(fontSize: 18, height: 1.4),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

檢查清單

  • [ ] 查看偵錯平台,觀察是否有輸出:
    🎙️ 收到 Audio Chunk ... ➔ 📩 收到即時逐字稿 ...

上一篇
Day 10:OpenAI Whisper API 串接與 HTTP/WebSocket 傳輸機制
下一篇
Day 12:處理多國語音/韓語與日語聲學辨識的參數調校
系列文
從行程到應援:30 天做出追星實用工具箱 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言